from typing import Any, Dict
import uuid
import arrow
import json
import bson
import time
import logging
import datetime
import ipaddress
import numpy as np
import copy
import os
from functools import wraps
from json.encoder import JSONEncoder
from django.conf import settings
from utilities.target import check_availability_is_normal

from wass.display import TaskType_NAME_MAP, LANGUAGE_CODE
from wass.dns_default import category_map, description_map
from utilities.regex import (
    SPECIAL_CHA_SET,
    IP_RE,
    IP_SEGMENT_RE,
    URL_LIST_RE,
    NAME_REPLACE_CHA_SET,
)
from utilities.enums import (
    TaskType,
    ReverseConfidenceMap,
    REVERSE_SEVERITY_MAP,
    SEVERITY_SCORE_MAP,
    SEVERITY_MAP,
    DATETIME_FORMAT,
    EmailTemplate,
    DATETIME_FORMAT_2,
    SecurityEventPlugins,
    AvailabilityNormalStatus,
)
from functools import reduce

logger = logging.getLogger(__name__)


JOB_NAME_QUERY_MAP = {
    'securityEvent': {'job_names': {'$elemMatch': {'$eq': 'appscan'}}},
    'vul': {'job_names': {'$elemMatch': {'$eq': 'kscan'}}},
    'availability': {
        'job_names': {'$elemMatch': {'$eq': 'site_info'}},
        'job_config_map.site_info.modules': {'$elemMatch': {'$ne': 'basic_info'}},
    },
    'target_info': {
        'job_names': {'$elemMatch': {'$eq': 'site_info'}},
        'job_config_map.site_info.modules': {'$elemMatch': {'$eq': 'basic_info'}},
    },
}


class DateTimeEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, datetime.datetime):
            return arrow.Arrow.fromdatetime(obj).for_json()
        else:
            return json.JSONEncoder.default(self, obj)


def make_id():
    return uuid.uuid1().hex


def get_task_type_query(task_type_list: list) -> Dict[str, Any]:
    query = {}
    if 'securityEvent' in task_type_list:
        query.update({'job_names': {'$elemMatch': {'$eq': 'appscan'}}})
    if 'vul' in task_type_list:
        query.update({'job_names': {'$elemMatch': {'$eq': 'kscan'}}})
    if 'availability' in task_type_list and 'target_info' in task_type_list:
        query.update({'job_names': {'$elemMatch': {'$eq': 'site_info'}}})
    else:
        if 'availability' in task_type_list:
            query.update(
                {
                    'job_names': {'$elemMatch': {'$eq': 'site_info'}},
                    'job_config_map.site_info.modules': {
                        '$elemMatch': {'$ne': 'basic_info'}
                    },
                }
            )
        if 'target_info' in task_type_list:
            query.update(
                {
                    'job_names': {'$elemMatch': {'$eq': 'site_info'}},
                    'job_config_map.site_info.modules': {
                        '$elemMatch': {'$eq': 'basic_info'}
                    },
                }
            )

    return query


# def get_job_name(task_type: str) -> str:
#     return JOB_NAME_MAP.get(task_type, '')


# def get_task_type_tuple() -> tuple:
#     return tuple(JOB_NAME_MAP.keys())


def filter_cha(s0: str) -> str:
    for c in SPECIAL_CHA_SET:
        s0 = s0.replace(c, rf'\{c}')

    return s0


def replace_cha(s0: str) -> str:
    for c in NAME_REPLACE_CHA_SET:
        s0 = s0.replace(c, '')

    return s0


def event_filter(event: dict, f: dict) -> bool:
    for k, v in f.items():
        v = int(v)
        event_value = event.get(k)
        if v == 0 and (event_value != 0 or event_value is not None):
            return False
        elif v != event_value:
            return False

    return True


def b2m(num):
    """B to KB,MB,GB,TB"""
    if num >= 1024 * 1024 * 1024 * 1024:
        return '%.2fTB' % round(num / (1024 * 1024 * 1024 * 1024), 2)
    elif num >= 1024 * 1024 * 1024:
        return '%.2fGB' % round(num / (1024 * 1024 * 1024), 2)
    elif num >= 1024 * 1024:
        return '%.2fMB' % round(num / (1024 * 1024), 2)
    elif num >= 1024:
        return '%.2fKB' % round(num / 1024, 2)
    else:
        return "%dB" % round(num, 2)


def check_ip_list(ip_list, check_type=str, limit=False):
    """
    校验IPv4格式
    """

    def ip_into_int(ip):
        return reduce(lambda x, y: (x << 8) + y, map(int, ip.split('.')))

    def _check_ip(ip):
        ip = ip.strip()
        tmp_list = ip.split('-')
        if len(tmp_list) == 2:
            begin, end = tmp_list[0].strip(), tmp_list[1].strip()
            if not IP_RE.match(begin) or not IP_RE.match(end):
                return False
            begin = ip_into_int(begin)
            end = ip_into_int(end)
            if begin >= end:
                return False
            if limit and (end - begin) > 255:
                return False
        elif len(tmp_list) == 1:
            if not IP_RE.match(ip) and not IP_SEGMENT_RE.match(ip):
                return False
        else:
            # 目前不支持三个以及以上的段...
            return False
        return ip

    ret_list = []
    for item in ip_list:
        if not item:
            continue
        if not isinstance(item, check_type):
            return False, []
        tmp = ''
        if check_type == list:
            ip = _check_ip(item[0])
            if ip:
                tmp = [ip] + item[1:]
        else:
            tmp = _check_ip(item)
        if tmp:
            ret_list.append(tmp)
        else:
            return False, []
    return True, ret_list


def check_ipv6_list(ip_list, check_type=str, limit=False):
    """校验IPv6格式"""

    def clean_ip(ip):
        def get_ipv6(ip):
            if not isinstance(ip, str):
                ip = str(ip)
            try:
                return ipaddress.IPv6Address(ip)
            except BaseException:
                pass

        ip = ip.strip()
        ips = ip.split('-')
        if len(ips) == 2:
            ipv6_0 = get_ipv6(ips[0])
            ipv6_1 = get_ipv6(ips[1])
            if ipv6_0 and ipv6_1:
                if ipv6_0 > ipv6_1:
                    return '{}-{}'.format(ips[1], ips[0])
                return ip
        elif len(ips) == 1:
            if get_ipv6(ips[0]):
                return ip

    ret_list = []
    for item in ip_list:
        if not item:
            continue
        if not isinstance(item, check_type):
            return False, []
        if check_type == list:
            cleaned_ip = clean_ip(item[0])
            if not cleaned_ip:
                return False, []
            cleaned_ip = [cleaned_ip, item[1]]
        else:
            cleaned_ip = clean_ip(item)
            if not cleaned_ip:
                return False, []
        ret_list.append(cleaned_ip)
    return True, ret_list


def is_url_list_match(item):
    if any(
        [
            item == '^$',
            item.startswith('^*'),
            item.endswith('*$'),
            item.find('**') != -1,
            not URL_LIST_RE.match(item),
        ]
    ):
        return False
    return True


def get_severity_name_query(severity_name, task_type):
    if task_type == TaskType.vul.value:
        return {'detail.severity': REVERSE_SEVERITY_MAP.get(severity_name, 'UNKNOWN')}
    elif task_type == TaskType.risk_link.value:
        low, high = ReverseConfidenceMap.get(severity_name, (0, 0))
        return {'detail.confidence': {'$gte': low, '$lt': high}}
    elif task_type == TaskType.securityEvent.value and severity_name == SEVERITY_MAP[2]:
        return {'severity': {'$in': [1, 2]}}
    else:
        return {'severity': REVERSE_SEVERITY_MAP.get(severity_name, 'UNKNOWN')}


class EventParser(object):
    """
    :return
    severity_name_count_map
    event_dict
    task_type_event_name_count_map
    task_type_severity_map
    risk_detail
    score
    """

    def __init__(self, instance_parser_list: list, include_plugins=None, use_fill=True):
        self.instance_parser_list = instance_parser_list
        self.site_info_conf = None
        self.target_id = None
        self.targets = None
        self.include_plugins = include_plugins or []
        self.use_fill = use_fill

        self.use_risk_detail = False

    def set_option(self, use_risk_detail: bool):
        self.use_risk_detail = use_risk_detail

    def get(self):
        current_level = 1
        score = -1

        severity_name_count_map = {}
        event_dict = {}
        task_type_event_name_count_map = {}
        task_type_severity_map = {}
        risk_detail = {}

        if self.use_fill:
            severity_name_count_map = {item: 0 for item in SEVERITY_MAP.values()}

        for instance_parser in self.instance_parser_list:
            task_type = instance_parser.task_type
            temp_task_type_severity_map = {}
            temp_task_type_event_name_count_map = {}
            temp_risk_detail = {}

            if self.use_fill:
                temp_task_type_severity_map = {
                    item: 0 for item in SEVERITY_MAP.values()
                }

            for event in instance_parser.event_list:
                severity = instance_parser.get_severity(event)
                # Default 0
                if score == -1:
                    score = 0
                if severity == -1:
                    continue
                severity_name = SEVERITY_MAP.get(severity, 0)
                if severity > current_level:
                    current_level = severity
                    score_section = SEVERITY_SCORE_MAP.get(current_level)
                    score = score_section[0]
                elif severity == current_level:
                    score += 1

                event['severity_name'] = severity_name

                # Get event name.
                event_name = event.get('event_name', '')
                new_event_name = None
                func = getattr(instance_parser, f"parse_{event_name}", None)
                if func:
                    detail, new_event_name = func(event)
                    if detail is None:
                        continue
                else:
                    detail = event.get('detail', {})

                event['detail'] = detail

                if new_event_name:
                    event['event_name'] = new_event_name

                severity_name_count_map[severity_name] = (
                    severity_name_count_map.get(severity_name, 0) + 1
                )
                temp_task_type_severity_map[severity_name] = (
                    temp_task_type_severity_map.get(severity_name, 0) + 1
                )
                event_dict.setdefault(task_type, []).append(event)
                func = getattr(instance_parser, 'get_count', None)

                if func:
                    k, v = func(event)
                    count = temp_task_type_event_name_count_map.get(k, 0)
                    temp_task_type_event_name_count_map[k] = count + v

                if self.use_risk_detail:
                    func = getattr(instance_parser, 'get_risk_detail_name', None)
                    if func:
                        risk_detail_name = func(event)
                        if risk_detail_name:
                            _severity_name = event.get('severity_name')
                            count = temp_risk_detail.setdefault(
                                risk_detail_name, {}
                            ).get(_severity_name, 0)
                            temp_risk_detail[risk_detail_name][_severity_name] = (
                                count + 1
                            )

            score_section = SEVERITY_SCORE_MAP.get(current_level)
            max_score = score_section[1]
            if score > max_score:
                score = max_score

            task_type_severity_map[task_type] = temp_task_type_severity_map
            task_type_event_name_count_map[
                task_type
            ] = temp_task_type_event_name_count_map
            if temp_risk_detail:
                risk_detail[task_type] = temp_risk_detail

        if self.use_fill:
            task_type_event_name_count_map = self.fill_task_type_event_name_count_map(
                task_type_event_name_count_map
            )

        return {
            'severity_name_count_map': severity_name_count_map,
            'event_dict': event_dict,
            'task_type_event_name_count_map': task_type_event_name_count_map,
            'task_type_severity_map': task_type_severity_map,
            'risk_detail': risk_detail,
            'score': score if score <= 100 else 100,
        }

    def fill_task_type_event_name_count_map(self, task_type_event_name_count_map):
        for k, v in task_type_event_name_count_map.items():
            for func_name in self.__dir__():
                if func_name == f"_fill_{k}":
                    task_type_event_name_count_map[k] = getattr(self, func_name)(v)

        return task_type_event_name_count_map

    def _fill_securityEvent(self, event_dict):
        new_event_dict = {
            SecurityEventPlugins.broken_links.value: event_dict.get(
                SecurityEventPlugins.broken_links.value, 0
            )
        }
        for item in self.include_plugins:
            # Prefix
            if item == 'statistics':
                item = SecurityEventPlugins.broken_links.value
            new_event_dict[item] = event_dict.get(item, 0)

        return new_event_dict

    @staticmethod
    def _fill_vul(event_dict):
        new_event_dict = {}
        for item in SEVERITY_MAP.values():
            new_event_dict[item] = event_dict.get(item, 0)
        return new_event_dict

    def _fill_risk_link(self, event_dict):
        return self._fill_vul(event_dict)

    def _fill_change_check(self, event_dict):
        return self._fill_vul(event_dict)


class CustomJSONEncoder(JSONEncoder):
    def default(self, o):
        if isinstance(o, datetime.datetime):
            return o.strftime("%Y-%m-%d %H:%M:%S")
        if isinstance(o, bson.ObjectId):
            return str(o)
        if isinstance(o, bson.DBRef):
            return str(o)
        if isinstance(o, bytes):
            return o.decode()
        if isinstance(o, (np.int64, np.int32, np.int16, np.int8)):
            return int(o)
        return super(CustomJSONEncoder, self).default(o)


def get_log_info(request):
    request_data = dict(request.data)
    request_query_params = dict(request.query_params)
    request_url = request.get_raw_uri()
    request_user = str(request.user)
    request_method = request.method
    log_msg = (
        f'RequestMethod:{request_method},RequestUrl:{request_url},'
        f'RequestQueryParams:{request_query_params},RequestData:{request_data},'
        f'RequestUser:{request_user}'
    )
    return log_msg


def get_event_email_data(task_type: str, event_name_count_map: dict) -> dict:
    assert task_type not in [TaskType.availability.value]

    s_list = []
    total = 0
    name_dict = TaskType_NAME_MAP.get(LANGUAGE_CODE, {}).get(task_type, {})
    task_type_name = name_dict.get('name', '')  # type: ignore
    name_map = name_dict.get('map', {}).get(LANGUAGE_CODE, {})  # type: ignore

    for k, count in event_name_count_map.items():
        s_list.append(f"{name_map.get(k, 'unknown')}{count}个")
        total += count

    s0 = ', '.join(s_list)

    exception_name = "风险URL"
    template = EmailTemplate.detail.value
    email_exception_name = f"存在{task_type_name}"

    if task_type == TaskType.vul.value:
        exception_name = "漏洞"
    elif task_type == TaskType.change_check.value:
        exception_name = f"地址疑似被{task_type_name}信息"
        template = EmailTemplate.change_check.value
        email_exception_name = f"疑似被{task_type_name}信息"

    return {
        'task_type_name': task_type_name,
        'exception_name': exception_name,
        'total': total,
        'data': s0,
        'template': template,
        'email_content': s0,
        'warning_content': f"发现存在{total}个{exception_name}",
        'inbox_content': f"发现存在{total}个{exception_name}",
        'email_exception_name': email_exception_name,
    }


def get_event_email_data_availability(parser, is_check_availability, is_check_cloud):
    detail = {}
    if parser.event_list:
        detail = parser.event_list[0].get('detail', {})
    table_html, abnormal_count = get_table_html(
        detail, parser, is_check_availability, is_check_cloud
    )

    return {
        'total': abnormal_count,
        'table': table_html,
        'exception_name': '可用性节点异常',
        'task_type_name': '可用性',
        'template': EmailTemplate.table.value,
        'email_content': table_html,
        'warning_content': f"发现{abnormal_count}个监测点响应异常",
        'inbox_content': f"共发现{abnormal_count}个监测点响应异常",
        'email_exception_name': '不可访问',
    }


def iso_format_to_local(isodatetime: str, t=0) -> str:
    arrow_datetime = arrow.Arrow.fromtimestamp(
        arrow.get(isodatetime).timestamp(), tzinfo=settings.TZ_INFO
    )
    if t == 1:
        return arrow_datetime.datetime.strftime(DATETIME_FORMAT_2)
    return arrow_datetime.datetime.strftime(DATETIME_FORMAT)


def get_table_html(detail, parser, is_check_availability, is_check_cloud):
    tbody = None

    normal_status = AvailabilityNormalStatus.default.value
    abnormal_count = 0

    # local
    if is_check_availability:
        key_tuple = ('ping', 'http_get', 'dns', 'http_get_full_time')
        tr = f"<tr><td>本地</td>"

        status = 0
        for key in key_tuple:
            if key == 'ping':
                time = detail.get(key, -1)
            else:
                time = detail.get(key, {}).get('time', -1)

            if key == 'http_get_full_time':
                status = detail.get(key, {}).get('status', 0)
                normal_status = getattr(parser, f'check_{key}')(detail)

            if key == 'http_get':
                td = f"<td><span>{time}ms/{int(status)}</span></td>"
            else:
                td = f"<td><span>{time}ms</span></td>"
            tr = f"{tr}{td}"

        if normal_status == AvailabilityNormalStatus.normal.value:
            span_class = 'green'
            span_text = '正常'
        else:
            span_class = 'red'
            span_text = '异常'
            abnormal_count += 1
        td = f"<td><span style='{span_class}'></span><span>{span_text}</span></td>"
        tr = f"{tr}{td}"
        tr = f"{tr}</tr>"
        if tbody:
            tbody = f"{tbody}{tr}"
        else:
            tbody = tr

    # cloud
    if is_check_cloud:
        detail_http = detail.get('cloud', {}).get('detail_http', {})
        detail_ping = detail.get('cloud', {}).get('detail_ping', {})

        length_http = len(detail_http)
        length_ping = len(detail_ping)

        # merge
        area_dict = {}
        for i in range(max(length_http, length_ping)):
            if i < length_http:
                area_dict.setdefault(detail_http[i].get('area'), {})[
                    'http'
                ] = detail_http[i]
            if i < length_ping:
                area_dict.setdefault(detail_ping[i].get('area'), {})[
                    'ping'
                ] = detail_ping[i]

        # loop
        for area, data in area_dict.items():
            tr = f"<tr><td>{area}</td>"

            dns_time = data.get('http', {}).get('dns_time', -1)
            http_resp_time = data.get('http', {}).get('resp_time', -1)
            ping_resp_time = data.get('ping', {}).get('resp_time', -1)
            download_time = data.get('http', {}).get('download_time', -1)
            status_code = data.get('http', {}).get('status_code', 0)
            status_code = int(status_code)
            node_is_normal = bool(check_availability_is_normal(status_code))
            node_normal_status = (
                AvailabilityNormalStatus.normal.value
                if node_is_normal
                else AvailabilityNormalStatus.abnormal.value
            )
            if node_normal_status == AvailabilityNormalStatus.normal.value:
                node_span_class = 'green'
                node_span_text = '正常'
            else:
                node_span_class = 'red'
                node_span_text = '异常'
                abnormal_count += 1

            tds = f"""
            <td><span>{ping_resp_time}ms</span></td>
            <td><span>{http_resp_time}ms/{status_code}</span></td>
            <td><span>{dns_time}ms</span></td>
            <td><span>{download_time}ms</span></td>
            <td><span style='{node_span_class}'></span><span>{node_span_text}</span></td>
            """

            tr = f"{tr}{tds}</tr>"
            if tbody:
                tbody = f"{tbody}{tr}"
            else:
                tbody = tr

    return tbody, abnormal_count


def check_time_format(s0: str) -> bool:
    s_list = s0.split(':')
    if len(s_list) != 3:
        return False

    if not (0 <= int(s_list[0]) < 24):
        return False

    for s in s_list[1:]:
        if not (0 <= int(s) < 60):
            return False

    return True


def get_host_ip() -> str:
    p = os.popen(
        "ifconfig -a| grep inet|grep -v 127.0.0.1| grep -v inet6| awk '{print $2}'| tr -d 'addr:'"
    )
    host = p.read()
    p.close()
    return host.replace('\n', '')


def trans_to_isodate(dt):
    if dt:
        return arrow.get(dt).isoformat()
    else:
        return ""


def trans_textbox_to_dict(text):
    d0 = {}
    if not text:
        return d0
    if isinstance(text, dict):
        return text

    for item in text.split('\n'):
        k_v_list = item.split(': ')
        if k_v_list:
            d0[k_v_list[0]] = k_v_list[1]
    return d0


def check_license_limit(config, license_config, _t='gte'):
    for k, v in license_config.items():
        config_value = config.get(k)
        license_value = license_config.get(k)
        if not config_value or not license_value:
            continue
        if value_compare(config_value, license_value, _t):
            config[k] = license_value

    return config


def value_compare(v0, v1, t):
    """
    gt: config >= license_config
    lt:
    gte:
    lte:
    eq:
    :param v0:
    :param v1:
    :param t:
    :return:
    """
    check_dict = {
        'gt': bool(v0 > v1),
        'lt': bool(v0 < v1),
        'gte': bool(v0 >= v1),
        'lte': bool(v0 <= v1),
        'eq': bool(v0 == v1),
    }

    return check_dict[t]


def clear_dict(tmp_dict: dict, new_dict=None) -> dict:
    """
    Remove '$' keys form dict.
    :param tmp_dict:
    :param new_dict:
    :return:
    """
    if not new_dict:
        new_dict = {}
    for k, v in tmp_dict.items():
        if k.startswith('$'):
            new_dict.pop(k, None)
            continue
        new_dict[k] = copy.deepcopy(v)
        if isinstance(v, dict):
            clear_dict(v, new_dict[k])

    return new_dict


def get_flat_value(d0, k: str, default=None):
    temp_k_list = k.split('.')
    if isinstance(d0, dict):
        temp_d = d0.get(temp_k_list[0])
    else:
        temp_d = d0[int(temp_k_list[0])]

    if temp_d is not None:
        if len(temp_k_list) > 1 and (
            isinstance(temp_d, dict) or isinstance(temp_d, list)
        ):
            return get_flat_value(temp_d, '.'.join(temp_k_list[1:]), default)
        return temp_d
    else:
        return default


def flat(x):
    for key, value in x.items():
        if isinstance(value, dict):
            for k, v in flat(value):
                k = f'{key}.{k}'
                yield k, v
        else:
            yield key, value


def get_clean_dict(d0: dict, allow_keys) -> dict:
    tmp_dict = {}
    for k, v in d0.items():
        if k in allow_keys:
            tmp_dict[k] = v

    return tmp_dict


def get_include_plugins_from_ts(task_session) -> list:
    if hasattr(task_session, 'to_dict'):
        task_session = task_session.to_dict()
    return [
        y
        for x in task_session.get('job_config_map', {}).values()
        for y in x.get('include_plugins') or x.get('plugins')
    ]


def admin_permission_check(obj_target_user, obj_request_user) -> bool:
    if obj_target_user.is_superuser and obj_request_user.username != 'admin':
        return False
    return True


def parse_malscan(detail):
    details = []
    for description_key, description in description_map.items():
        description_infos = detail.get("staticinfo", {}).get(description_key, [])
        for description_info in description_infos:
            for vul_key in description_info:
                if vul_key == "url":
                    continue
                _description = description.get(vul_key, {}).get("zh-cn", "")
                category = category_map[description_key]
                detail_item = {
                    "desc": _description,
                    "url": list(description_info.keys())[0] if list(description_info.keys()) else "",
                    "type": category,
                }
                details.append(detail_item)
    return details


def get_affects_url(refer_path, target):
    data = {"affects": "", "addition": {"outURL": ""}}
    if isinstance(refer_path, list):
        for index in range(len(refer_path))[::-1]:
            elem = refer_path[index]
            if elem.startswith(target):
                data["affects"] = elem
                data["addition"]["outURL"] = (
                    "" if index == len(refer_path) - 1 else refer_path[index + 1]
                )
                break
    else:
        data["affects"] = refer_path
        data["addition"]["outURL"] = "" if refer_path.startswith(target) else refer_path

    return data


def get_paginate(request):
    page = int(request.GET.get('page', 1))
    limit = int(request.GET.get('limit', 10))

    if page < 1:
        page = 1
    if limit < 1:
        limit = 1
    return page, limit


def validate_task_config_time(task_config):
    scan_period_start_time = task_config.scan_period_start_time
    scan_period_end_time = task_config.scan_period_end_time
    now = arrow.now(settings.TZ_INFO).datetime
    now_date = now.strftime("%Y-%m-%d")
    if scan_period_start_time:
        start_time_str = "{} {}".format(now_date, scan_period_start_time)
        start_time = arrow.get(start_time_str, tzinfo=settings.TZ_INFO).datetime
        if start_time > now:
            return "每天监测时间段不在指定的周期监测时间段范围内"
    if scan_period_end_time:
        end_time_str = "{} {}".format(now_date, scan_period_end_time)
        end_time = arrow.get(end_time_str, tzinfo=settings.TZ_INFO).datetime
        if end_time < now:
            return "每天监测时间段不在指定的周期监测时间段范围内"


def retry(max_retries=3, delay=1):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            retries = 0
            while retries < max_retries:
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    logger.error(f"Retry {retries+1}/{max_retries} - Exception: {e}")
                    retries += 1
                    time.sleep(delay)
            logger.warning("Max retries exceeded. Giving up.")

        return wrapper

    return decorator
